package up6.store;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.jsp.JspWriter;

import org.apache.commons.lang.StringUtils;
import org.bouncycastle.util.encoders.Hex;

import up6.Md5Tool;

/**
 * 
 * AWS签名算法4
 * 	https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-string-to-sign.html
 CreateMultipartUpload
	https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/API_CreateMultipartUpload.html
 PutObject
	https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
 * @author qwl
 *
 */
public class MinioAuthorization {
	
	private static final String String = null;
	private MinioConfig cfg;
	private String timeCur="";//20220519
	private String timeIso="";	
	private String method="PUT";//PUT,GET,POST
	private String uri="";//资源路径,/test/test.txt
	private String host="";
	public String data_sha256_Hash="";//内容-sha256   HexEncode(Hash(RequestPayload))
	public String dataMd5="";
	public int contentLength=0;
	private Date timeNow= new Date();
	private String contentType="";
	private Map<String,String> m_headers=new HashMap<String,String>();
	private String CanonicalQueryString;//请求字符串

	/**
	 * 构造
	 */
	public MinioAuthorization() {

		this.timeIso = this.getTimeIso8601();
		DateFormat df = new SimpleDateFormat("yyyyMMdd");
		this.timeCur= df.format(this.timeNow);
		
		//header
		this.m_headers.put("content-length", "");
		this.m_headers.put("content-type", "");
		this.m_headers.put("host", "");
		this.m_headers.put("content-md5", "");
		this.m_headers.put("x-amz-content-sha256", "");
		this.m_headers.put("x-amz-date", "");
	}
	
	public MinioAuthorization setConfig(MinioConfig v) {
		this.cfg = v;
		return this;
	}
	
	private String getHeaders() {
		String head = this.CanonicalHeaders();
		String[] arr = head.split("\n");
		List<String> heads=new ArrayList<String>();
		
		for(String a : arr)
		{
			if(!a.isEmpty())
			{
				int end = a.length()-1;
				if(a.indexOf(":")!=-1)end = a.indexOf(":");
				heads.add(a.substring(0,end));
			}
		}
		head= StringUtils.join(heads.toArray(),";");
		//System.out.println("标头："+head);
		return head;
	}
	
	public MinioAuthorization setMethod(String v) {this.method = v;
	return this;}

	public MinioAuthorization setData(byte[] v) {
		this.contentLength = v.length;
		this.data_sha256_Hash = Md5Tool.base16( Md5Tool.sha256(v) );
		this.dataMd5 = Md5Tool.getMD5(v);
		this.dataMd5 = this.dataMd5.substring(8,24);
		try {
			this.dataMd5 = Base64.getEncoder().encodeToString(this.dataMd5.getBytes("utf-8"));
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return this;
	}	
	public String getDataHash() {return this.data_sha256_Hash;}
	public MinioAuthorization setUrl(String v) {
		try {
			URL u = new URL(v);
			this.host = u.getHost();
			if( u.getPort() != 80 )
			{
				this.host += ":";
				this.host += u.getPort();				
			}
			
			this.setUri(u.getPath());
			this.setQueryString(u.getQuery());
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return this;
	}
	
	/**
	 * 参考：
	 * https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/sigv4-query-string-auth.html
	 * URI encode every byte except the unreserved characters: 'A'-'Z', 'a'-'z', '0'-'9', '-', '.', '_', and '~'.
	 * The space character is a reserved character and must be encoded as "%20" (and not as "+").
	 * Each URI encoded byte is formed by a '%' and the two-digit hexadecimal value of the byte.
	 * Letters in the hexadecimal value must be uppercase, for example "%1A".
	 * Encode the forward slash character, '/', everywhere except in the object key name. For example, if the object key name is photos/Jan/sample.jpg, the forward slash in the key name is not encoded.
	 * @param v
	 * @return
	 */
	public MinioAuthorization setUri(String v)
	{
		try {
			v = URLEncoder.encode(v,"UTF-8");
			v = v.replace("+", "%20");
			v = v.replace("*", "%2A");
			v = v.replace("%7E", "~");
			v = v.replace("%2F", "/");
			this.uri= v;
			//System.out.println("uri编码：\n"+this.uri);
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return this;
	}
	public MinioAuthorization setHost(String h) {this.host=h;
	return this;}
	public MinioAuthorization setContentType(String v) {this.contentType=v;
	return this;}
	public MinioAuthorization setQueryString(String v) {
		if(null==v ) return this;
		if( v.isEmpty()) return this;
		//?uploads -> uploads		
		String[] arr = v.split("&");
		List<String> qs = new ArrayList<String>();
		for(String q : arr)
		{
			//所有参数后面必须跟=
			if(q.indexOf("=")==-1) q += "=";
			qs.add(q);
		}
		
		this.CanonicalQueryString = StringUtils.join(qs.toArray(), "&");
		return this;
		}
	
	public MinioAuthorization delHead(String n) {
		this.m_headers.remove(n.toLowerCase());
		return this;
	}
	
	public MinioAuthorization addHead(String n,String v) {
		this.m_headers.put(n, v);
		return this;
	}
	
	/**
	 * 获取ISO时间
	 * @return
	 */
	private String getTimeIso8601() {
        DateFormat df = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        df.setTimeZone(TimeZone.getTimeZone("UTC"));
        String t = df.format(this.timeNow);
        return t;
	}
	
	public String getTimeIso() {
		return this.timeIso;
	}
	
	byte[] HmacSHA256(String data, byte[] key) throws Exception 
	{
	    String algorithm="HmacSHA256";
	    Mac mac = Mac.getInstance(algorithm);
	    mac.init(new SecretKeySpec(key, algorithm));
	    byte[] arr= mac.doFinal(data.getBytes("UTF-8"));
	    //return Hex.encode(arr);
	    return arr;
	}
    
	/**
	 * 签名密钥 
	 * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-calculate-signature.html
	 * 	AWS4-HMAC-SHA256
		20150830T123600Z
		20150830/us-east-1/iam/aws4_request
		示例签名密钥
			f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59
	 * @param key
	 * @param dateStamp
	 * @param regionName
	 * @param serviceName
	 * @return
	 * @throws Exception
	 */
	byte[] getSignatureKey() throws Exception 
	{	
	    byte[] kSecret = ("AWS4" + this.cfg.sk).getBytes();
	    byte[] kDate = HmacSHA256(this.timeCur, kSecret);
	    byte[] kRegion = HmacSHA256(this.cfg.region, kDate);
	    byte[] kService = HmacSHA256(this.cfg.service, kRegion);
	    byte[] kSigning = HmacSHA256("aws4_request", kService);	        

	    return kSigning;
	}
	
	/**
	 * 规范标头
	 * @return
	 */
	String CanonicalHeaders() {
		String[] arr = {
				"content-length:"+this.contentLength,
				//"content-md5:"+this.dataMd5,
				"content-type:"+this.contentType,
				"host:"+this.host,
				"x-amz-content-sha256:"+this.data_sha256_Hash,
				//"x-amz-content-sha256:"+this.data_sha256_Hash,
				"x-amz-date:"+this.timeIso
		};
		
		//设置header值
		if(this.m_headers.containsKey("content-length"))
			this.m_headers.put("content-length",String.valueOf(this.contentLength) );
		if(this.m_headers.containsKey("content-type"))
			this.m_headers.put("content-type",this.contentType );
		if(this.m_headers.containsKey("host"))
			this.m_headers.put("host",this.host );
		if(this.m_headers.containsKey("content-md5"))
			this.m_headers.put("content-md5",this.dataMd5 );
		if(this.m_headers.containsKey("x-amz-content-sha256"))
			this.m_headers.put("x-amz-content-sha256",this.data_sha256_Hash );
		if(this.m_headers.containsKey("x-amz-date"))
			this.m_headers.put("x-amz-date",this.timeIso );
		
		List<String> hs = new ArrayList<String>();
		for(Object k : this.m_headers.keySet())
		{
			Object v = this.m_headers.get(k);
			hs.add(k+":"+v);
		}
		Collections.sort(hs);
		
		String str = StringUtils.join(hs.toArray(),"\n")+"\n";
		//System.out.println("规范标头："+str);
		return str;
	}
	
	/**
	 * 凭证范围值，
	 * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-string-to-sign.html
	 * 示例：
	 * 	20150830/us-east-1/iam/aws4_request
	 * @return
	 */
	String CredentialScope() {
		String[] arr= {
				this.timeCur,
				this.cfg.region,
				"s3/aws4_request"
		};
		String str= StringUtils.join(arr,"/");
		//System.out.println("凭证范围：\n"+str);

		return str;
	}
	
	/**
	 * 规范请求头签名
	 * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-canonical-request.html
	 * 格式：
	 * CanonicalRequest =
		  HTTPRequestMethod + '\n' +
		  CanonicalURI + '\n' +
		  CanonicalQueryString + '\n' +
		  CanonicalHeaders + '\n' +
		  SignedHeaders + '\n' +
		  HexEncode(Hash(RequestPayload))
	 * @return
	 */
	String HashCanonicalRequest() {
		String[] arr= {
				this.method,
				this.uri,
				this.CanonicalQueryString,//请求字符串
				this.CanonicalHeaders(),//请求头参数
				this.getHeaders(),//签名头
				this.data_sha256_Hash//内容-sha256
		};

		String str = StringUtils.join(arr, "\n");			
		//System.out.println("规范请求头：\n"+str);
		str = Md5Tool.sha256(str);
		//System.out.println("规范请求头签名："+str);
		return str;
	}
		
	/**
	 * 待签名字符串
	 * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-create-string-to-sign.html
	 * 格式：
	 * StringToSign =
	    Algorithm + \n +
	    RequestDateTime + \n +
	    CredentialScope + \n +
	    HashedCanonicalRequest
	 * @return
	 */
	private String StringSign() {
		String[] arr= {
				this.cfg.algorithm,
				this.timeIso,
				this.CredentialScope(),
				this.HashCanonicalRequest()
		};
		//待签名字符串
		String str = StringUtils.join(arr, "\n");
		try {

			//System.out.println("待签名字符串：\n"+str);
			//计算签名密钥
			byte[] key = this.getSignatureKey();
			{
				//System.out.println("签名密钥："+Md5Tool.base16(key) );
			}
			//签名
			str = Md5Tool.base16(  this.HmacSHA256(str, key));
			//System.out.println("密钥编码："+str);			
			
		} catch (Exception e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		return str;
	}
	
	/**
	 * 待签名字符串
	 * @return
	 */
	public String Credential() {
		String[] arr= {
				this.cfg.ak,//
				this.timeCur,//时间：20220519
				this.cfg.region,//region
				this.cfg.service,//服务
				"aws4_request"
		};
		String v = StringUtils.join(arr, "/");
		//System.out.println("待签名字符串："+v);
		return v;
	}
	
	/**
	 * Authorization 标头,添加到Header中
	 * https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4-add-signature-to-request.html
	 * 格式：
	 * 	Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
	 * 示例：
	 * 	Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
	 * @return
	 */
	public String Authorization() {
	return this.cfg.algorithm + " " +
			"Credential=" + this.Credential() + ", "+
			"SignedHeaders="+this.getHeaders()+", "+//签名头
			"Signature=" + this.StringSign();
	}	
}
